// Main.cs by steb at 1:32 PM 8/10/2008
//
//
// Copyright (C) 2008 Steven Allen
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
//

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Collections;
using System.Reflection;
namespace XiimServer
{
    class MainClass
    {
        public static Queue dataQ = null;       // Create the send queue
        public static ManualResetEvent tcpClientConnected = new ManualResetEvent(false); // For threading
        public static ArrayList connections = new ArrayList(); // The list of connections
        public static ArrayList banned = new ArrayList(); // The list of connections
        public static IPAddress localAddr;
        // A struct that includes the stream and username of each connection
        public struct Connection
        {
            public NetworkStream stream;
            public String uname;
            public String ip;
            public TcpClient client;
        }
        static void Main(string[] args)
        {
			localAddr = Dns.GetHostEntry(Dns.GetHostName()).AddressList[0];
			Int32 port = 13913;
			string help = "Xiim Server v1.0\nCopyright 2008 Steven Allen\n  --help,-h\t\tThis help\n  --ip [IP Address]\tThe IP address on which to listen"; 
			for (short x=0; x<args.Length; x++)
			{
				switch(args[x])
				{
				 case "--ip":
					try
					{
						x++;
						localAddr = IPAddress.Parse(args[x]);
					} catch {
						Console.WriteLine("Invalid IP Address");
						Console.Write(help);
						return;
					}
					break;
				case "--port":
				case "-p":
					try 
					{
						x++;
						port = Int32.Parse(args[x]);
					} catch {
						Console.WriteLine("Invalid Port");
						Console.Write(help);
						return;
					}
					break;
				case "--help":
				case "-h":
					Console.Write(help);
					return;
				default:
					Console.WriteLine("Invalid argument: " + args[x]);
					Console.Write(help);
					return;
				}
			}
                     // Set the port
            // Initialize the send queue, server, and send thread
            dataQ = new Queue();
            TcpListener server = new TcpListener(localAddr, port);
            Thread send = new Thread(sendData);
            // Try to start the server and the send thread then wait for connections
            try
            {
                // Start listening for client requests.
                server.Start();
                send.Start();
                Console.WriteLine("Started Xiim server on " + localAddr);
                while (true)
                {
                    connectWait(server);
                }
            }
            catch (SocketException e)
            {
                Console.WriteLine("SocketException: {0}", e);
            }
            finally
            {
                // Stop listening for new clients.
                server.Stop();
            }
        }
        // Wait for connections and run connectAccept
        public static void connectWait(TcpListener server)
        {
                tcpClientConnected.Reset();
                Console.WriteLine("Waiting for connection...");
                server.BeginAcceptTcpClient(new AsyncCallback(connectAccept), server);
                tcpClientConnected.WaitOne();
        }
        // Accept connections
        public static void connectAccept(IAsyncResult ar)
        {
            // Set the server and client
            TcpListener server = (TcpListener)ar.AsyncState;
            TcpClient client = server.EndAcceptTcpClient(ar);
            // If IP is banned: dissconnect
            string ip = client.Client.RemoteEndPoint.ToString().Substring(0, client.Client.RemoteEndPoint.ToString().IndexOf(':'));
            foreach (Connection this_connection in banned)
            {
                if (this_connection.ip == ip)
                {
                    client.Close();
                    tcpClientConnected.Set();
                    return;
                }
            }
            // Add them to an arraylist
            ArrayList server_client = new ArrayList(2);
            server_client.Add(server);
            server_client.Add(client);
            // Pass this arrayList into the connected thread
            Thread con = new Thread(connected);
            con.Start(server_client);
            tcpClientConnected.Set();
        }
        public static void connected(object server_client)
        {
            // Create new connection struct
            Connection connection = new Connection();
            // Read in the client and server
            ArrayList server_client2 = (ArrayList) server_client;
            TcpClient client = (TcpClient)server_client2[1];
            // Add client to the connection Connection
            connection.client = client;
            // Get the client's IP address
            connection.ip = client.Client.RemoteEndPoint.ToString().Substring(0, client.Client.RemoteEndPoint.ToString().IndexOf(':'));
            Console.WriteLine("Connected to " + connection.ip);
            // Initialize variables for storing data
            Byte[] bytes = new Byte[256];
            String data = null;
            // Get a stream object for reading and writing
            NetworkStream stream = client.GetStream();
            // and add it to the Connection connection
            connection.stream = stream;
            // Wait for the username and add it to the Connection connection
            connection.uname = System.Text.Encoding.ASCII.GetString(bytes, 0, stream.Read(bytes, 0, bytes.Length));
            // Check if the username is already used or if it contains forbidden charictars.
			// If so set the username to annonymous
			if (connection.uname.Contains("/") || connection.uname.Contains("\\") || connection.uname.Contains(":") || connection.uname.Contains("@") || connection.uname.Contains("<"))		
			{			
				connection.uname = "Annonymous";
			}
			else
			{
				foreach (Connection connection_other in connections)
	            {
	                if (connection.uname.ToUpper() == connection_other.uname.ToUpper())
	                {
	                    connection.uname = "Annonymous";
	                }
	            }
			}
            // Add connection to connections arrayList
            connections.Add(connection);
            dataQ.Enqueue(":: " + connection.uname + " joined ::");
            // Loop to receive all the data sent by the client.
            int i;
            try
            {
                while ((i = stream.Read(bytes, 0, bytes.Length)) != 0)
                {
                    data = System.Text.Encoding.ASCII.GetString(bytes, 0, i);
                    if (data.StartsWith("/"))
                    {

                        String command = (data.Contains(':')) ? data.Substring(1, data.IndexOf(':') - 1).ToUpper() : data.Substring(1).ToUpper();
                        String arg = data.Substring(data.IndexOf(':') + 1, ((data.Contains(' ')) ? data.IndexOf(' ') : data.Length) - data.IndexOf(':') - 1).ToUpper();
                        data = (data.Contains(' ')) ? data.Remove(0, data.IndexOf(' ')) : null;
                        switch (command)
                        {
                            case "TO":
                                foreach (Connection this_connection in connections)
                                {
                                    if (this_connection.uname.ToUpper() == arg)
                                    {
                                        sendDataTo(connection.uname + " >> " + this_connection.uname + ":" + data, this_connection.stream);
                                        Thread.Sleep(1);
                                        sendDataTo(connection.uname + " >> " + this_connection.uname + ":" + data, connection.stream);
                                        break;
                                    }
                                }
                                break;
                            case "KICK":
                                if (connection.ip == localAddr.ToString())
                                {
                                    foreach (Connection this_connection in connections)
                                    {
                                        if (this_connection.uname.ToUpper() == arg)
                                        {
                                            dataQ.Enqueue(":: " + arg + " was kicked ::");
                                            Thread.Sleep(100);
                                            disconnect(this_connection);
                                            Console.WriteLine("Kicked " + arg);
                                            break;
                                        }
                                    }
                                }
                                else
                                    sendDataTo(":: You don't have permission to do this :: ", connection.stream);
                                break;
                            case "BAN":
                                if (connection.ip == localAddr.ToString())
                                {
                                    foreach (Connection this_connection in connections)
                                    {
                                        if (this_connection.uname.ToUpper() == arg)
                                        {
                                            dataQ.Enqueue(":: " + arg + " was banned ::");
                                            Thread.Sleep(100);
                                            banned.Add(this_connection);
                                            disconnect(this_connection);
                                            Console.WriteLine("Banned " + arg);
                                            break;
                                        }
                                    }
                                }
                                else
                                    sendDataTo(":: You don't have permission to do this :: ", connection.stream);
                                break;
                            case "UNBAN":
                                if (connection.ip == localAddr.ToString())
                                {
                                    foreach(Connection this_connection in banned)
                                    {
                                        if (this_connection.uname.ToUpper() == arg)
                                        {
                                            dataQ.Enqueue(":: " + arg + " was unbanned ::");
                                            banned.Remove(this_connection);
                                            break;
                                        }
                                    }
                                }
                                else
                                    sendDataTo(":: You don't have permission to do this :: ", connection.stream);
                                break;
                            case "LIST":
                                data = ":: User List ::\r\n";
                                foreach (Connection this_connection in connections)
                                {
                                    data += this_connection.uname + "\r\n";
                                }
                                data += ":: End User List ::";
                                sendDataTo(data, connection.stream);
                                break;
                            case "LISTBAN":
                                if (connection.ip == localAddr.ToString())
                                {
                                    data = ":: Banned List ::\r\n";
                                    foreach (Connection this_connection in banned)
                                    {
                                        data += this_connection.uname + "\r\n";
                                    }
                                    data += ":: End Banned List ::";
                                    sendDataTo(data, connection.stream);
                                }
                                else
                                    sendDataTo(":: You don't have permission to do this :: ", connection.stream);
                                break;
                            case "HELP":
                                data = ":: HELP ::\r\n";
                                data += "/to:[user] [message]\tSends [message] to [user]\r\n";
                                data += "/list\t\t\tLists Users\r\n";
                                if (connection.ip == localAddr.ToString())
                                {
                                    data += "/listban\t\tLists Banned Users\r\n";
                                    data += "/ban:[user]\t\tBans [user]\r\n";
                                    data += "/unban:[user]\t\tUnbans [user]\r\n";
                                    data += "/kick:[user]\t\tKicks [user]\r\n";
                                }
                                data += ":: END HELP ::";
                                sendDataTo(data, connection.stream);
                                break;
                            default:
                                sendDataTo(":: \"" + command + "\" is not a command :: ", connection.stream);
                                break;
                        }
                    }
                    else
                    {
                        // Translate data bytes to a ASCII string.
                        data = connection.uname + ": " + data;
						//Console.WriteLine("Got data from " + connection.ip + ".");
                        // Add the data to the send Q
                        dataQ.Enqueue(data);
                    }
                }
            }
            catch (System.IO.IOException)
            {
                // Close client and stream and remove the connection from the connections arraylist
                disconnect(connection);
                Console.WriteLine("Error: Connection to " + connection.ip + " forcibly closed.\n");
            }
            catch (System.ObjectDisposedException)
            {
                // Close client and stream and remove the connection from the connections arraylist
                Console.WriteLine(connection.ip + "was disconnected");
            }
            finally
            {
                // Close client and stream and remove the connection from the connections arraylist
                disconnect(connection);
                Console.WriteLine("Connection to " + connection.ip + " Closed.\n");
                dataQ.Enqueue(":: " + connection.uname + " left ::");
            }
        }
        // Waits for data in the dataQ Queue and sends it to all of the clients
        public static void sendData()
        {
            while (true)
            {
                if (dataQ.Count > 0)
                {
                    String data = (string) dataQ.Dequeue();
                    lock (connections)
                    {
                        foreach (Connection connection in connections)
                        {
                            byte[] msg = System.Text.Encoding.ASCII.GetBytes(data);
                            connection.stream.Write(msg, 0, msg.Length);
                        }
                    }
                    //Console.WriteLine("Sent data.");
                }
                Thread.Sleep(1);
            }
        }
        public static void sendDataTo(String data, NetworkStream stream)
        {
            byte[] msg = System.Text.Encoding.ASCII.GetBytes(data);
            stream.Write(msg, 0, msg.Length);
            //Console.WriteLine("Sent data to user.");
        }
        public static void disconnect(Connection close_connection)
        {
            lock (connections)
            {
                close_connection.stream.Close();
                close_connection.client.Close();
                connections.Remove(close_connection);
            }
        }
    }
}
